<?php

/**
 * Decorator builder class
 *
 * @author     Mon Zafra <monzee at gmail>
 * @copyright  (c)2009 Mon Zafra
 * @category   Mz
 * @license    http://mz-project.googlecode.com/svn/trunk/LICENSE    MIT License
 * @version    SVN: $Id: Decorate.php 16 2009-04-07 02:10:14Z monzee $
 */
class Mz_Decorate
{
    protected $_aliases = array();
    protected $_defaultOpts;

    /**
     * constructor.
     *
     * @param array $defaults Default options appended to every decorator
     */
    public function __construct(array $defaults = array('placement' => 'append'))
    {
        $this->_defaultOpts = $defaults;
    }

    /**
     * Clears the alias cache.
     */
    public function reset()
    {
        $this->_aliases = array();
    }

    /**
     * Generates a decorator factory param, with a proper alias and default
     * options appended to the specified options.
     *
     * Everything must pass through this function to ensure that no alias would
     * be reused and the placement option is set to 'append' (via the default
     * options).
     *
     * @param <type> $type
     * @param <type> $opts
     * @param <type> $alias
     * @return array
     */
    public function generateDecorator($type, $opts = array(), $alias = null)
    {
        if (null === $alias) {
            $alias = $type;
        }
        $i = 1;
        $base = $alias;
        while (in_array($alias, $this->_aliases)) {
            $alias = $base . '_' . $i++;
        }
        $this->_aliases[] = $alias;

        $decorator = $type == $alias ? $type : array($alias => $type);

        $opts = (array) $opts;
        $options = $opts + $this->_defaultOpts;

        return array('decorator' => $decorator, 'options' => $options);
    }

    /**
     * Passed by the text() method, consumed by the Callback decorator.
     *
     * @param string $content
     * @param Zend_Form_Element_Abstract $elem
     * @param array $opts
     * @return string
     */
    static public function textCallback($content, $elem, $opts)
    {
        $text = isset($opts['text']) ? $opts['text'] : '';
        if (isset($opts['escape']) && $opts['escape']) {
            $view = $elem->getView();
            if ($view instanceof Zend_View_Abstract) {
                $text = $view->escape($text);
            } else {
                $text = htmlentities($text);
            }
        }
        return $text;
    }

    /**
     * Forward all other calls to generateDecorator().
     *
     * The method name is used as the $type parameter.
     *
     * @param string $method
     * @param array  $args
     * @return array
     */
    public function __call($method, $args)
    {
        $opts = isset($args[0]) ? $args[0] : array();
        $alias = isset($args[1])? $args[1] : null;
        $type = strtoupper($method[0]) . substr($method, 1);
        return $this->generateDecorator($type, $opts, $alias);
    }

    /**
     * Returns a callback decorator. The callback returns an arbitrary text.
     *
     * Pass the arbitrary text as the first parameter or as part of an array
     * indexed by 'text'. Text is escaped by default.
     *
     * @param string|array $opts Possible options: 'text', 'escape'
     * @param string $alias
     * @return array
     */
    public function text($opts, $alias = 'text')
    {
        if (is_string($opts)) {
            $opts = array('text' => $opts);
        }
        $opts['callback'] = array($this, 'textCallback');
        $opts = $opts + array('escape' => true);
        return $this->generateDecorator('Callback', $opts, $alias);
    }

    /**
     * Wraps decorators with HtmlTag decorators on both ends.
     *
     * Specify the decorators to be wrapped as separate parameters.
     *
     * @param string|array $opts The tag itself or an array of options to be
     *                           passed to the HtmlTag decorator.
     * @return array
     */
    public function wrap($opts)
    {
        if (is_string($opts)) {
            $opts = array('tag' => $opts);
        }

        if (!is_array($opts)) {
            throw new InvalidArgumentException('The first parameter must be a string or an array');
        } else if (!isset($opts['tag'])) {
            $opts['tag'] = 'div';
        }

        if (isset($opts['alias'])) {
            $alias = $opts['alias'];
            unset($opts['alias']);
        } else {
            $alias = 'wrap';
        }

        unset($opts['openOnly'], $opts['closeOnly']);

        $ret = array();

        $openOpts = $closeOpts = $opts;
        $openOpts['openOnly'] = true;
        $closeOpts['closeOnly'] = true;

        $ret[] = $this->generateDecorator('HtmlTag', $openOpts, $alias . 'Open');

        $args = func_get_args();
        array_shift($args);
        $elems = call_user_func_array(array($this, 'aggregate'), $args);
        $ret = array_merge($ret, $elems);

        $ret[] = $this->generateDecorator('HtmlTag', $closeOpts, $alias . 'Close');

        return $ret;
    }

    /**
     * Combine arrays of decorators into a single array.
     *
     * Pass individual decorators as separate parameters. E.g.
     * $this->aggregate('Label', 'ViewHelper', 'Errors', $this->text('foo'));
     * Use decorator names only or the return values of methods of this class.
     *
     * @return array
     */
    public function aggregate()
    {
        $elements = func_get_args();
        $ret = array();
        foreach ($elements as $elem) {
            if (is_string($elem)) {
                $ret[] = $this->generateDecorator($elem);
            } else if (is_array($elem)) {
                if ('decorator' === key($elem)) {
                    // this element is a single decorator and must be appended
                    $ret[] = $elem;
                } else {
                    // a set of decorators; its elements should be merged
                    // instead of appended
                    $ret = array_merge($ret, $elem);
                }
            }
        }
        return $ret;
    }

}
